/**
 * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
 * Copyright (C) 2016-2020 the AndroidAnnotations project
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed To in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.ohosannotations.api;

import ohos.eventhandler.EventRunner;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.miscservices.timeutility.Time;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 背景执行人
 *
 * @author dev
 * @since 2021-07-21
 */
public final class BackgroundExecutor {
    /**
     * 默认的错误的线程监听器
     */
    public static final WrongThreadListener DEFAULT_WRONG_THREAD_LISTENER = new WrongThreadListener() {
        @Override
        public void onUiExpected() {
            throw new IllegalStateException("Method invocation is expected from the UI thread");
        }

        @Override
        public void onBgExpected(String... expectedSerials) {
            if (expectedSerials.length == 0) {
                throw new IllegalStateException("Method invocation is expected from a background thread,"
                    + " but it was called from the UI thread");
            }
            throw new IllegalStateException("Method invocation is expected from one of serials "
                + Arrays.toString(expectedSerials) + ", but it was called from the UI thread");
        }

        @Override
        public void onWrongBgSerial(String currentSerial, String... expectedSerials) {
            if (currentSerial == null) {
                currentSerial = "anonymous";
            }
            throw new IllegalStateException("Method invocation is expected from one of serials "
                + Arrays.toString(expectedSerials) + ", but it was called from " + currentSerial + " serial");
        }
    };

    /**
     * 默认的执行者
     */
    public static final Executor DEFAULT_EXECUTOR =
        Executors.newScheduledThreadPool(2 * Runtime.getRuntime().availableProcessors());
    private static final HiLogLabel TAG = new HiLogLabel(HiLog.LOG_APP, 0x000001, "BackgroundExecutor");

    private static Executor executor = DEFAULT_EXECUTOR;
    private static WrongThreadListener wrongThreadListener = DEFAULT_WRONG_THREAD_LISTENER;
    private static final List<Task> TASKS = new ArrayList<>();
    private static final ThreadLocal<String> CURRENT_SERIAL = new ThreadLocal<>();

    private BackgroundExecutor() {
    }

    /**
     * Execute a runnable after the given delay.
     *
     * @param runnable the task to execute
     * @param delay the time from now to delay execution, in milliseconds
     * <p>
     * if <code>delay</code> is strictly positive and the current
     * executor does not support scheduling (if
     * {@link #setExecutor(Executor)} has been called with such an
     * executor)
     * @return Future associated to the running task
     */
    private static Future<?> directExecute(Runnable runnable, long delay) {
        Future<?> future = null;
        if (delay > 0) {
            /* no serial, but a delay: schedule the task */
            if (!(executor instanceof ScheduledExecutorService)) {
                throw new IllegalArgumentException("The executor set does not support scheduling");
            }
            ScheduledExecutorService scheduledExecutorService = (ScheduledExecutorService) executor;
            future = scheduledExecutorService.schedule(runnable, delay, TimeUnit.MILLISECONDS);
        } else {
            if (executor instanceof ExecutorService) {
                ExecutorService executorService = (ExecutorService) executor;
                future = executorService.submit(runnable);
            } else {
                /* non-cancellable task */
                executor.execute(runnable);
            }
        }
        return future;
    }

    /**
     * Execute a task after (at least) its delay <strong>and</strong> after all
     * tasks added with the same non-null <code>serial</code> (if any) have
     * completed execution.
     *
     * @param task the task to execute
     */
    public static synchronized void execute(Task task) {
        if (task.id != null || task.serial != null) {
            /* keep task */
            TASKS.add(task);
        }
        if (task.serial == null || !hasSerialRunning(task.serial)) {
            task.executionAsked = true;
            task.future = directExecute(task, task.remainingDelay);
        }
    }

    /**
     * Execute a task.
     *
     * @param runnable the task to execute
     * @param id identifier used for task cancellation
     * @param delay the time from now to delay execution, in milliseconds
     * @param serial the serial queue (<code>null</code> or <code>""</code> for no
     * serial execution)
     */
    public static void execute(final Runnable runnable, String id, long delay, String serial) {
        execute(new Task(id, delay, serial) {
            @Override
            public void execute() {
                runnable.run();
            }
        });
    }

    /**
     * Execute a task after the given delay.
     *
     * @param runnable the task to execute
     * @param delay the time from now to delay execution, in milliseconds
     */
    public static void execute(Runnable runnable, long delay) {
        directExecute(runnable, delay);
    }

    /**
     * Execute a task.
     *
     * @param runnable the task to execute
     */
    public static void execute(Runnable runnable) {
        directExecute(runnable, 0);
    }

    /**
     * Execute a task after all tasks added with the same non-null
     * <code>serial</code> (if any) have completed execution.
     * <p>
     * Equivalent to {@link #execute(Runnable, String, long, String)
     * execute(runnable, id, 0, serial)}.
     *
     * @param runnable the task to execute
     * @param id identifier used for task cancellation
     * @param serial the serial queue to use (<code>null</code> or <code>""</code> for
     * no serial execution)
     */
    public static void execute(Runnable runnable, String id, String serial) {
        execute(runnable, id, 0, serial);
    }

    /**
     * Change the executor.
     * <p>
     * Note that if the given executor is not a {@link ScheduledExecutorService}
     * then executing a task after a delay will not be supported anymore. If it is
     * not even a {@link ExecutorService} then tasks will not be cancellable
     * anymore.
     *
     * @param executor the new executor
     */
    public static void setExecutor(Executor executor) {
        BackgroundExecutor.executor = executor;
    }

    /**
     * Changes the default {@link WrongThreadListener}. To restore the default one
     * use {@link #DEFAULT_WRONG_THREAD_LISTENER}.
     *
     * @param listener the new {@link WrongThreadListener}
     */
    public static void setWrongThreadListener(WrongThreadListener listener) {
        wrongThreadListener = listener;
    }

    /**
     * Cancel all tasks having the specified <code>id</code>.
     *
     * @param id the cancellation identifier
     * @param mayInterruptIfRunning <code>true</code> if the thread executing this task should be
     * interrupted; otherwise, in-progress tasks are allowed to complete
     */
    public static synchronized void cancelAll(String id, boolean mayInterruptIfRunning) {
        for (int i = TASKS.size() - 1; i >= 0; i--) {
            Task task = TASKS.get(i);
            if (id.equals(task.id)) {
                if (task.future != null) {
                    task.future.cancel(mayInterruptIfRunning);
                    if (!task.managed.getAndSet(true)) {
                        /*
                         * the task has been submitted to the executor, but its execution has not
                         * started yet, so that its run() method will never call postExecute()
                         */
                        task.postExecute();
                    }
                } else if (task.executionAsked) {
                    HiLog.warn(TAG, "A task with id " + task.id
                        + " cannot be cancelled (the executor set does not support it)");
                } else {
                    /* this task has not been submitted to the executor */
                    TASKS.remove(i);
                }
            }
        }
    }

    /**
     * Checks if the current thread is UI thread and notifies
     * {@link WrongThreadListener#onUiExpected()} if it doesn't.
     */
    public static void checkUiThread() {
        if (EventRunner.getMainEventRunner().getThreadId() != Thread.currentThread().getId()) {
            wrongThreadListener.onUiExpected();
        }
    }

    /**
     * Checks if the current thread is a background thread and, optionally,
     * restricts it with passed serials. If no serials passed and current thread is
     * the UI thread, then {@link WrongThreadListener#onBgExpected(String...)} will
     * be called. If the current thread is not UI and serials list is empty, then
     * this method just returns. Otherwise, if the method was called not during
     * {@link Task} execution or the task has no serial, then the
     * {@link WrongThreadListener#onWrongBgSerial(String, String...)} will be called
     * with null for the first parameter. If task has a serial but passed serials
     * don't contain that, then
     * {@link WrongThreadListener#onWrongBgSerial(String, String...)} will be called
     * with the task's serial for the first parameter.
     *
     * @param serials (optional) list of allowed serials
     */
    public static void checkBgThread(String... serials) {
        if (serials.length == 0) {
            if (EventRunner.getMainEventRunner().getThreadId() == Thread.currentThread().getId()) {
                wrongThreadListener.onBgExpected(serials);
            }
            return;
        }
        String current = CURRENT_SERIAL.get();
        if (current == null) {
            wrongThreadListener.onWrongBgSerial(null, serials);
            return;
        }
        for (String serial : serials) {
            if (serial.equals(current)) {
                return;
            }
        }
        wrongThreadListener.onWrongBgSerial(current, serials);
    }

    /**
     * Indicates whether a task with the specified <code>serial</code> has been
     * submitted to the executor.
     *
     * @param serial the serial queue
     * @return boolean
     */
    private static boolean hasSerialRunning(String serial) {
        for (Task task : TASKS) {
            if (task.executionAsked && serial.equals(task.serial)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Retrieve and remove the first task having the specified <code>serial</code>
     * (if any).
     *
     * @param serial the serial queue
     * @return task if found, <code>null</code> otherwise
     */
    private static Task take(String serial) {
        int len = TASKS.size();
        for (int i = 0; i < len; i++) {
            if (serial.equals(TASKS.get(i).serial)) {
                return TASKS.remove(i);
            }
        }
        return null;
    }

    /**
     * Task
     *
     * @since 2021-07-22
     */
    public static abstract class Task implements Runnable {
        private String id;
        private long remainingDelay;
        private long targetTimeMillis; /* since epoch */
        private String serial;
        private boolean executionAsked;
        private Future<?> future;

        /*
         * A task can be cancelled after it has been submitted to the executor but
         * before its run() method is called. In that case, run() will never be called,
         * hence neither will postExecute(): the tasks with the same serial identifier
         * (if any) will never be submitted.
         *
         * Therefore, cancelAll() *must* call postExecute() if run() is not started.
         *
         * This flag guarantees that either cancelAll() or run() manages this task post
         * execution, but not both.
         */
        private AtomicBoolean managed = new AtomicBoolean();

        /**
         * Task
         *
         * @param id id
         * @param delay delay
         * @param serial serial
         */
        public Task(String id, long delay, String serial) {
            if (!"".equals(id)) {
                this.id = id;
            }
            if (delay > 0) {
                remainingDelay = delay;
                targetTimeMillis = Time.getRealTime() + delay;
            }
            if (!"".equals(serial)) {
                this.serial = serial;
            }
        }

        /**
         * run
         */
        @Override
        public void run() {
            if (managed.getAndSet(true)) {
                /* cancelled and postExecute() already called */
                return;
            }

            try {
                CURRENT_SERIAL.set(serial);
                execute();
            } finally {
                /* handle next tasks */
                postExecute();
            }
        }

        /**
         * execute
         */
        public abstract void execute();

        private void postExecute() {
            if (id == null && serial == null) {
                /* nothing to do */
                return;
            }
            CURRENT_SERIAL.set(null);
            synchronized (BackgroundExecutor.class) {
                /* execution complete */
                TASKS.remove(this);

                if (serial != null) {
                    Task next = take(serial);
                    if (next != null) {
                        if (next.remainingDelay != 0) {
                            /* the delay may not have elapsed yet */
                            next.remainingDelay = Math.max(0L, next.targetTimeMillis - Time.getRealTime());
                        }
                        /* a task having the same serial was queued, execute it */
                        BackgroundExecutor.execute(next);
                    }
                }
            }
        }
    }

    /**
     * A callback interface to be notified when a method invocation is expected from
     * another thread.
     *
     * @see #setWrongThreadListener(WrongThreadListener)
     * @see #checkUiThread()
     * @see #checkBgThread(String...)
     * @see org.ohosannotations.annotations.SupposeUiThread
     * @see org.ohosannotations.annotations.SupposeBackground
     */
    public interface WrongThreadListener {
        /**
         * Will be called, if the method is supposed to be called from the UI-thread,
         * but was called from a background thread.
         *
         * @see org.ohosannotations.annotations.SupposeUiThread
         * @see #setWrongThreadListener(WrongThreadListener)
         * @see #DEFAULT_WRONG_THREAD_LISTENER
         */
        void onUiExpected();

        /**
         * Will be called, if the method is supposed to be called from a background
         * thread, but was called from the UI-thread.
         *
         * @param expectedSerials a list of allowed serials. If any background thread is allowed the
         * list will be empty.
         * @see org.ohosannotations.annotations.SupposeBackground
         * @see #setWrongThreadListener(WrongThreadListener)
         * @see #DEFAULT_WRONG_THREAD_LISTENER
         */
        void onBgExpected(String... expectedSerials);

        /**
         * Will be called, if the method is supposed to be called from a background
         * thread with one of {@code expectedSerials}, but was called from a
         * {@code currentSerial}. {@code currentSerial} will be null, if it is called
         * from a background thread without a serial.
         *
         * @param currentSerial the serial of caller thread or null if there is no serial
         * @param expectedSerials a list of allowed serials
         * @see org.ohosannotations.annotations.SupposeBackground
         * @see #setWrongThreadListener(WrongThreadListener)
         * @see #DEFAULT_WRONG_THREAD_LISTENER
         */
        void onWrongBgSerial(String currentSerial, String... expectedSerials);
    }
}
